import time

import numpy as np
from OCC.Core.BRep import BRep_Tool
from OCC.Core.Tesselator import ShapeTesselator
from OCC.Core.TopAbs import TopAbs_SOLID, TopAbs_FACE, TopAbs_SHAPE, TopAbs_COMPOUND, TopAbs_COMPSOLID, TopAbs_SHELL, \
    TopAbs_VERTEX, TopAbs_EDGE, TopAbs_WIRE
from OCC.Core.TopExp import TopExp_Explorer, topexp_MapShapes
from OCC.Core.TopTools import TopTools_IndexedMapOfShape
from OCC.Core.TopoDS import topods_Face, topods_Solid, topods_CompSolid, topods_Compound, TopoDS_Compound, TopoDS_Solid, \
    TopoDS_Face, TopoDS_CompSolid, TopoDS_Shell, TopoDS_Shape, topods_Shell, topods_Vertex

import json as json_d
import queue
import math
import re
import copy

from OCC.Extend.TopologyUtils import discretize_edge, discretize_wire

float32 = np.dtype("<f4")
unit32 = np.dtype("<u4")
unit8 = np.dtype("<u1")
_magic = {"gl_tf": 1179937895,
          "json": 1313821514,
          "bin": 5130562}
facesList = []
edge_Dict = {}

def get_normal(step_file,position):
    f=open(step_file,'r')
    datalines = f.readlines()
    datalines = [line for line in datalines if line[0] == '#']
    f.close()
    pattern = re.compile(r'#\d+')
    line_normal = None
    line_dict = {}
    for line in datalines:
        gg = line.split('=')
        line_dict[gg[0]] = gg[1]
    dataline = [line for line in datalines if position in line]
    assert len(dataline) > 0
    line_corr = dataline[0].split("(")[1:]
    lineid = line_corr[1].split(')')[0]
    line_data = line_dict[lineid]
    line_id = pattern.findall(line_data)
    assert len(line_id) > 0
    line_data = line_dict[line_id[0]]
    item_pattern = re.compile(r'[(]#\d+[)]')
    item_list = item_pattern.findall(line_data)
    assert len(item_list) == 2
    vertex1 = line_dict[item_list[0][1:-1]]
    line_corr = vertex1.split("(")[1:]
    coor = line_corr[1].split(')')[0]
    coordinate = coor.split(',')[0:]
    coordinate1 = np.array([float(s) for s in coordinate])

    vertex2 = line_dict[item_list[1][1:-1]]
    line_corr = vertex2.split("(")[1:]
    coor = line_corr[1].split(')')[0]
    coordinate = coor.split(',')[0:]
    coordinate2 = np.array([float(s) for s in coordinate])

    line_normal = coordinate2 - coordinate1
    # print("line_normal",line_normal)
    # print("line_normal",list(line_normal))
    return list(line_normal)

def get_vertex(step_file,position):
    f=open(step_file,'r')
    datalines = f.readlines()
    f.close()
    vertex = None
    for data in datalines:
        if 'CARTESIAN_POINT' in data and position in data:
            line_corr = data.split("(")[1:]
            name = line_corr[0].split("'")[1]
            coor = line_corr[1].split(')')[0]
            coordinate = coor.split(',')[0:]
            coordinate = [float(s) for s in coordinate]
            vertex = coordinate
            return vertex


def get_all_extra_info(step_file_name):
    pointpattern = re.compile(r'(\'AP-.*\')')
    linepattern = re.compile(r'NL-\d{2}')
    rpspattern = re.compile(r'RPS')
    f = open(step_file_name, 'r')
    datalines = f.read()
    f.close()
    vertex_list = pointpattern.findall(datalines)
    rps_list=rpspattern.findall(datalines)
    vertex_list = [v[1:-1] for v in vertex_list]
    vertex_list.extend(rps_list)
    nl_list = linepattern.findall(datalines)
    part_json_dict={}
    part_json_dict['points'] = []
    part_json_dict['lines'] = []

    for v in vertex_list:
        vertex_dict = {}
        vertex_dict['name'] = v
        vertex_dict['points'] =get_vertex(step_file_name,v)
        part_json_dict['points'].append(vertex_dict)
    for nl in nl_list:
        normal_dict = {}
        normal_dict['name'] = nl
        normal_dict['normal']= get_normal(step_file_name,nl)
        part_json_dict['lines'].append(normal_dict)
    return part_json_dict


class ShapeToGlb:
    """
    :param shape: OCC-shape
    :param path:  文件路径
    :param filename:    文件名
    :param step_file step文件
    :param color:   颜色
    :param exp_type:   解析实体的
    :return: gltf文件
    """

    def __init__(self, shape=None, path=None, filename=None, step_file=None,exp_type=None, color=None, part_id=1):
        # print('ShapeToGlb=============================', part_id)
        self.shape = shape
        self.path = path
        self.filename = filename
        self.color = color
        self.part_id = part_id
        self.materials = {"line_material": [0, 0, 0, 1]}  # 材质
        self.geometries = []
        self.exp_type = exp_type
        self.side = False
        self.vertices = []
        self.normals = []
        self.step_file = step_file
        self.position_list = []
        self.obj_zon = Container.ModelObject()
        # part_json_dict=get_all_extra_info(step_file)
        # self.obj_zon.extras['assemble_pt']=part_json_dict['points']
        # self.obj_zon.extras['assemble_line']=part_json_dict['lines']
        self.config_model_info(self.obj_zon, "GLTF Model", 'json')
        self.obj_zon.children = []
        self.shape_to_date()

    @staticmethod
    def tess_shape(shape):
        #  解析OCC方法（离散）
        tess = ShapeTesselator(shape)
        tess.Compute(compute_edges=True, mesh_quality=6, parallel=True)
        vertices_position = tess.GetVerticesPositionAsTuple()
        vertices_normal = tess.GetNormalsAsTuple()
        edges = []
        for edge_number in range(tess.ObjGetEdgeCount()):
            single_edge = []
            for vertex_number in range(tess.ObjEdgeGetVertexCount(edge_number)):
                edge_vertices = tess.GetEdgeVertex(edge_number, vertex_number)
                for coord in edge_vertices:
                    single_edge.append(coord)
            edges.append(single_edge)

        return vertices_position, vertices_normal, edges

    @staticmethod
    def config_model_info(obj, name, transId, part_json_dict={}):
        obj.name = name
        obj.extras["transId"] = transId
        obj.extras["assemble_pt"] = part_json_dict.get('points')
        obj.extras["assemble_line"] = part_json_dict.get('lines')
        # print("obj.extras",obj.extras)

    def shape_to_date(self):
        if not isinstance(self.shape, dict):
            if self.color is None:
                self.color = [1, 1, 1]
            solids_map = TopTools_IndexedMapOfShape()
            topexp_MapShapes(self.shape, TopAbs_SOLID, solids_map)
            if solids_map.Size() == 0:
                topexp_MapShapes(self.shape, TopAbs_FACE, solids_map)
            part_json_dict = get_all_extra_info(self.step_file)
            print("part_json_dict", part_json_dict)
            for idx in range(1, solids_map.Size() + 1):
                print("idx", idx)
                obj = Container.ModelObject()
                shape = solids_map.FindKey(idx)
                # obj.extras["assemble_pt"] = part_json_dict.get('points')
                # obj.extras["assemble_line"] = part_json_dict.get('lines')
                self.config_model_info(obj, 'box{0}'.format(idx), 'instructions',part_json_dict)
                obj.children = []
                self.tess_to_glt(shape, obj)
                idx += 1
                # topExp.Next()
            self.process_data()
        else:
            idx = 0
            # topExp = TopExp_Explorer()
            part_json_dict = get_all_extra_info(self.step_file)

            for a_shape in self.shape:
                # print("a_shape", a_shape)
                s, c = self.shape[a_shape]
                print("self.shape", a_shape)
                self.color = [c.Red(), c.Green(), c.Blue()]
                # topExp.Init(a_shape, TopAbs_SOLID)
                # while topExp.More():
                obj = Container.ModelObject()
                # obj.extras["assemble_pt"] = part_json_dict.get('points')
                # obj.extras["assemble_line"] = part_json_dict.get('lines')
                # print("part_json_dict",part_json_dict)/
                # self.config_model_info(obj, 'object{0}'.format(1), 'describe')
                self.config_model_info(obj, 'box{}'.format(idx), 'instructions',part_json_dict)

                idx += 1
                obj.children = []
                self.tess_to_glt(a_shape, obj)
                # topExp.Next()
                # print("tess_shape", time.time() - t1)
            self.process_data()

    def tess_to_glt(self, shape, obj):
        face_num = 1
        faces_map = TopTools_IndexedMapOfShape()
        topexp_MapShapes(shape, TopAbs_SOLID, faces_map)
        # topExp1 = TopExp_Explorer()
        # topExp1.Init(shape, TopAbs_FACE)
        if faces_map.Size != 0:
            for face_num in range(1, faces_map.Size() + 1):
                # while topExp1.More():
                fc = faces_map.FindKey(face_num)
                vertices_position, vertices_normal, edges_line = self.tess_shape(fc)
                obj_a_shape = Container.ModelObject()
                self.config_model_info(obj_a_shape, 'face_%s' % face_num, face_num)
                key = "Material_" + str(Util.color_init(self.color))
                if key not in self.materials:
                    self.color.append(1)
                    self.materials[key] = self.color
                    obj_a_shape.mid = len(self.materials.values()) - 1
                else:
                    material_keys = self.materials.keys()
                    for i, m in enumerate(material_keys):
                        if m == key:
                            obj_a_shape.mid = i
                            break

                start_position = len(self.vertices)
                for pos in vertices_position:
                    self.vertices.append(pos)

                start_normal = len(self.normals)
                for nor in vertices_normal:
                    self.normals.append(nor)

                geometry = Container.Geometry()
                data = Container.GeometryData()
                data.v = [start_position, len(self.vertices)]
                data.n = [start_normal, len(self.normals)]
                geometry.data = data
                self.geometries.append(geometry)

                obj_a_shape.gid = len(self.geometries) - 1

                obj.children.append(obj_a_shape)
                # topExp1.Next()
                # face_num += 1

            edges_map2 = TopTools_IndexedMapOfShape()
            topexp_MapShapes(shape, TopAbs_EDGE, edges_map2)
            point_map = TopTools_IndexedMapOfShape()
            topexp_MapShapes(shape, TopAbs_VERTEX, point_map)
            # topExp2 = TopExp_Explorer()
            # topExp2.Init(shape, TopAbs_EDGE)
            for line_num in range(1, edges_map2.Size() + 1):
                # print(line_num)
                ed = edges_map2.FindKey(line_num)

                line_array = discretize_edge(ed)
                # print(line_array)
                line = Container.ModelObject()
                self.config_model_info(line, 'edge_{}'.format(line_num), face_num)
                # print('edge_{}'.format(line_num))
                obj.children.append(line)
                start_offset = len(self.vertices)
                for line_pos in line_array:
                    for point in line_pos:
                        self.vertices.append(float(point))
                line_geometry = Container.Geometry()
                line_geometry.type = 3
                data = Container.GeometryData()
                data.v = [start_offset, len(self.vertices)]
                data.n = None
                line_geometry.data = data

                self.geometries.append(line_geometry)
                line.gid = len(self.geometries) - 1
                line.mid = 0
            for point_num in range(1, point_map.Size() + 1):
                # print(line_num)
                vex_p = point_map.FindKey(point_num)
                point_array = BRep_Tool.Pnt(vex_p).Coord()
                # print(line_array)
                point_ = Container.ModelObject()
                self.config_model_info(point_, 'point_{}'.format(point_num), face_num)
                # print('edge_{}'.format(line_num))
                obj.children.append(point_)
                start_offset = len(self.vertices)
                for point in point_array:
                    self.vertices.append(float(point))
                point_geometry = Container.Geometry()
                point_geometry.type = 3
                data = Container.GeometryData()
                data.v = [start_offset, len(self.vertices)]
                data.n = None
                point_geometry.data = data

                self.geometries.append(point_geometry)
                point_.gid = len(self.geometries) - 1
                point_.mid = 0
            self.obj_zon.children.append(obj)

    def process_data(self):
        container = Container()
        container.geometries = self.geometries
        container.obj = self.obj_zon
        materials = []
        for key, item in self.materials.items():
            materials.append(Container.Material(key, item))
        # print('materials,材质', self.materials)
        container.materials = materials
        GltfConverter.vertices = self.vertices
        GltfConverter.normals = self.normals
        GltfConverter.needNormal = True
        converter = GltfConverter()
        converter.ConvertToGLTF(container, self.side, self.path, self.filename.split(".")[0])


class GltfConverter:  # 系统转gltf二进制
    vertices = []
    normals = []
    needNormal = True
    byte_list = []

    def __init__(self):
        self.meshAndMaterial = {}
        self.byteOffset = 0

    def ConvertToGLTF(self, container, doubleSide, path, fileName):
        self.doubleSide = doubleSide
        output = OutputJSON()
        output.nodes = []
        output.meshes = []
        self.processObject(container.obj, output.nodes)
        self.processMeshes(container.geometries, output)  # 几何体
        self.processMaterials(container.materials, output)  # 材料

        buffer_data = bytes().join(GltfConverter.byte_list)
        bin_header = _byte_pad(
            np.array([len(buffer_data), 0x004E4942], dtype="<u4").tobytes())
        output.buffers.append(Buffer(len(buffer_data)))
        content = json_d.dumps(
            output, default=filterNone)
        content += (4 - ((len(content) + 20) % 4)) * " "
        content = content.encode("utf-8")
        header = _byte_pad(
            np.array([_magic["gl_tf"],  # magic, turns into glTF
                      2,  # GLTF version
                      len(content) + len(buffer_data) + 28,
                      len(content),
                      1313821514],
                     dtype="<u4").tobytes())

        exported = bytes().join([header,
                                 content,
                                 bin_header,
                                 buffer_data])
        print('文件保存成功：' + path + fileName + '.glb')
        with open(path + fileName + '.glb', 'wb') as f:
            f.write(exported)
        GltfConverter.vertices = []
        GltfConverter.normals = []
        GltfConverter.needNormal = True
        GltfConverter.byte_list = []
        Container.obj = None
        Container.geometries = []
        Container.materials = []

    def processObject(self, obj, nodes):
        node = Node()
        nodes.append(node)
        node.name = obj.name
        node.mesh = obj.gid
        node.children = []

        if obj.extras is not None:
            node.extras = obj.extras
        if obj.mid is not None and obj.gid is not None:
            if int(obj.gid) in self.meshAndMaterial:
                q = self.meshAndMaterial[int(obj.gid)]
                q.put(int(obj.mid))
            else:
                q = queue.Queue()
                q.put(int(obj.mid))
                self.meshAndMaterial[int(obj.gid)] = q

        count = len(nodes)

        if obj.children is not None and len(obj.children) > 0:
            for item in obj.children:
                node.children.append(self.processObject(item, nodes))
        else:
            node.children = None

        return count - 1

    def processMeshes(self, geometries, output):
        for geometry in geometries:
            mesh = Mesh(geometry.uuid)
            mesh.primitives = []
            output.meshes.append(mesh)
            primitive = Primitive()
            if geometry.data.n is not None and GltfConverter.needNormal == True:
                primitive.mode = geometry.type
                attributes = Attribute()
                attributes.POSITION = self.addOutPutAccessors(
                    output, geometry.data.v, "VEC3", "POSITION")
                attributes.NORMAL = self.addOutPutAccessors(
                    output, geometry.data.n, "VEC3", "NORMAL")
                primitive.attributes = attributes
                primitive.material = self.meshAndMaterial[len(
                    output.meshes) - 1].get()
                mesh.primitives.append(primitive)
            else:
                primitive.mode = geometry.type
                primitive.attributes = Attribute()
                primitive.attributes.POSITION = self.addOutPutAccessors(
                    output, geometry.data.v, "VEC3", "POSITION")
                primitive.material = self.meshAndMaterial[len(
                    output.meshes) - 1].get()
                mesh.primitives.append(primitive)

    def processMaterials(self, materials, output):
        for material in materials:
            output.materials.append(Material(material.color, self.doubleSide))

    def getMinMax(self, v):
        array = np.array(GltfConverter.vertices[v[0]:v[1]]).reshape(-1, 3)
        print(array)
        data = {
            "min": array.min(axis=0).tolist(),
            "max": array.max(axis=0).tolist(),
        }
        return data

    def addOutPutAccessors(self, output, v, t, types):
        accessor = Accessor()
        accessor.componentType = ComponentType.FLOAT
        accessor.type = t
        accessor.count = (v[1] - v[0]) / 3
        output.accessors.append(accessor)

        if types == "POSITION":
            accessor.bufferView = self.AddbufferView(output, v, t, types)
            minMax = self.getMinMax(v)
            accessor.min = minMax["min"]
            accessor.max = minMax["max"]
        else:
            accessor.bufferView = self.AddbufferView(output, v, t, types)
            accessor.min = None
            accessor.max = None

        return len(output.accessors) - 1

    def AddbufferView(self, output, v, t, types):
        byteLength = self.getPaddedBufferSize((v[1] - v[0]) * 4)
        bufferView = BufferView()
        target = None
        if types == "POSITION":
            target = self.vertices
        else:
            target = self.normals
        bufferView.buffer = self.processBuffer(v, target)
        bufferView.byteOffset = self.byteOffset
        bufferView.byteLength = byteLength
        bufferView.byteStride = 12
        bufferView.target = 34962
        self.byteOffset += byteLength
        output.bufferViews.append(bufferView)
        return len(output.bufferViews) - 1

    def processBuffer(self, v, target):
        current = np.array(target[v[0]:v[1]])
        data = _byte_pad(current.astype(float32).tobytes())
        GltfConverter.byte_list.append(data)
        return 0

    def getPaddedBufferSize(self, v):
        return int(math.ceil(v / 4.0)) * 4


def _byte_pad(data, bound=4):
    """
    GLTF wants chunks aligned with 4 byte boundaries.
    This function will add padding to the end of a
    chunk of bytes so that it aligns with the passed
    boundary size.
    Parameters
    --------------
    data : bytes
      Data to be padded
    bound : int
      Length of desired boundary
    Returns
    --------------
    padded : bytes
      Result where: (len(padded) % bound) == 0
    """
    bound = int(bound)
    if len(data) % bound != 0:
        # extra bytes to pad with
        count = bound - (len(data) % bound)
        # bytes(count) only works on Python 3
        pad = (' ' * count).encode('utf-8')
        # combine the padding and data
        result = bytes().join([data, pad])
        # we should always divide evenly
        if (len(result) % bound) != 0:
            raise ValueError(
                'byte_pad failed! ori:{} res:{} pad:{} req:{}'.format(
                    len(data), len(result), count, bound))
        return result
    return data


GLB_HEADER_BYTES = 12
GLB_HEADER_MAGIC = 0x46546C67
GLB_VERSION = 2
GLB_CHUNK_PREFIX_BYTES = 8
GLB_CHUNK_TYPE_JSON = 0x4E4F534A
GLB_CHUNK_TYPE_BIN = 0x004E4942


class ComponentType:
    UNSIGNED_BYTE = 0x1401
    UNSIGNED_SHORT = 0x1403
    FLOAT = 0x1406
    UNSIGNED_INT = 0x1405


def filterNone(obj):
    m = {}
    dic = obj.__dict__
    for k, v in dic.items():
        if v is not None:
            m[k] = v
    return m


class Attribute:
    def __init__(self):
        self.POSITION = None
        self.NORMAL = None


class Scene:
    def __init__(self):
        self.nodes = [0]
        self.name = "GLTF-Model"


class PbrMetallicRoughness:
    def __init__(self, color):
        self.baseColorFactor = color
        self.metallicFactor = 0.0
        self.roughnessFactor = 0.0


class Material:
    def __init__(self, color, side):
        self.pbrMetallicRoughness = PbrMetallicRoughness(color)
        self.doubleSided = side


class Accessor:
    def __init__(self):
        self.bufferView = None
        self.componentType = None
        self.count = None
        self.type = None
        self.min = None
        self.max = None


class Asset:
    def __init__(self):
        self.version = "2.0"
        self.generator = "center"


class Buffer:
    uri = "model.bin"

    def __init__(self, byteLength):
        self.byteLength = byteLength


class OutputJSON:
    def __init__(self):
        self.scene = 0
        self.scenes = [Scene()]
        self.asset = Asset()
        self.nodes = []
        self.meshes = []
        self.accessors = []
        self.bufferViews = []
        self.buffers = []
        self.materials = []


class Node:
    def __init__(self):
        self.children = None
        self.mesh = None
        self.name = None
        self.extras = None


class Primitive:
    def __init__(self):
        self.attributes = None
        self.material = None
        self.mode = None


class Mesh:
    def __init__(self, name):
        self.name = name
        self.primitives = None


class BufferView:
    def __init__(self):
        self.buffer = None
        self.byteOffset = None
        self.byteLength = None
        self.byteStride = None
        self.target = None


class Util:
    @staticmethod
    def color_init(color):
        return ((int)(color[0] * 255) << 16) | ((int)(color[1] * 255) << 8) | (int)(color[2] * 255)


class ContFaceLine:
    children = []


class Container:  # 容器
    class Material:  # 材质
        def __init__(self, uuid, color):
            self.uuid = uuid
            self.color = color

    class Geometry:  #
        def __init__(self):
            self.data = None
            self.uuid = None
            self.type = None

    class GeometryData:  # 数据
        def __init__(self):
            self.v = []
            self.n = None

    class ModelObject:
        def __init__(self):
            self.name = ""
            self.type = None
            self.children = None
            self.gid = None
            self.mid = None
            self.extras = {}
            self.status = None

    obj = None
    geometries = []
    materials = []
